Skip to content

fix(rest): the dispatcher's /packages domain is the one implementation of the package read and delete routes (#14503) - #16628

Merged
os-zhuang merged 14 commits into
mainfrom
claude/issue-14503-package-routes-single-implementation
Sep 8, 2026
Merged

fix(rest): the dispatcher's /packages domain is the one implementation of the package read and delete routes (#14503)#16628
os-zhuang merged 14 commits into
mainfrom
claude/issue-14503-package-routes-single-implementation

Conversation

@claude

@claude claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #14503

Summary

GET /api/v1/packages, GET /api/v1/packages/:id and DELETE /api/v1/packages/:id now have one implementation: the runtime dispatcher's /packages domain (packages/runtime/src/domains/packages.ts). @objectstack/rest's registerPackageRoutes no longer mounts its own copies of those three routes; it mounts POST /api/v1/packages/publish and nothing else. This executes the maintainer's ruling on #14503 (2026-09-02, verbatim 「同意」, option A), in the ruled order: measure → verify compositions → delete with the survivor pinned.

The SHADOWS sentence the card is about, the opposite "mounts FIRST in the production stack" claim in the same file, the #7563 docblock's "independent of composition order" claim, and the two module headers calling the ?version= refusal "dormant behind a Hono collapse" are all taken with the deletion.

Step 1 — the census, re-taken on today's tree (ce8caba914, the branch base)

The prior census (comment 5544498087) was measured before PR #15859; every verdict below is a fresh reading with its control on the same corpus.

behaviour objectstack (production TS) Console (objectui @ pin a472b07) content/docs verdict
source: 'registry' | 'database' | 'both' readers 0 (10 grep hits: 3 unrelated source: 'database' on metadata-loader items, 5 in the producer itself, 2 SDK comments) — control .writable = 17 0 — control .writable = 26 0 EMPTY → deliberately removed, recorded in the changeset
?version= senders to a packages door 1ScopedEnvironmentClient.packages.get(id, version?) (packages/client/src/index.ts) — control ?overwrite= = 2 0 — control ?keepData= = 1 (the Console's DELETE sends the dispatcher's parameter) 0 one sender, of the parameter, not of the repeated-parameter refusal (nobody repeats it)

The SDK sender is already carded: #12034 (open, pm:awaiting-maintainer) holds the maintainer's 2026-08-26 ruling that packages.get binds the bare row; its remaining half is exactly the scoped binding this PR now makes bindable. Nothing is ported into the dispatcher: a version-scoped durable lookup was a behaviour only the REST copy had, and porting it would change the dispatcher's data source — materially bigger than what was ruled.

Step 2 — compositions, and the fork

Every in-tree composition that mounts the REST package registrar also mounts the dispatcher: packages/cli/src/commands/serve.ts (createRestApiPlugin :4019, createDispatcherPlugin :4039), packages/verify/src/harness.ts (:594/:595), packages/plugins/plugin-dev/src/dev-plugin.ts (:856/:873). None turns enableProjectScoping on (mergeBootConfig forces it false on the standalone boot).

The deciding reading — driven through the real @objectstack/hono catch-all on the built runtime dist, at ce8caba914 (kernel: session-backed identity, ObjectQL engine with a real SchemaRegistry holding com.acme.crm and the permission-set tables the shared authz resolver reads):

404  CONTROL unscoped GET /api/v1/packages/no.such.package
       RESOURCE_NOT_FOUND :: Package 'no.such.package' not found
404  SCOPED   GET /api/v1/environments/env_alpha/packages/no.such.package
       RESOURCE_NOT_FOUND :: Package 'no.such.package' not found
200  SCOPED   GET /api/v1/environments/env_alpha/packages/com.acme.crm
       data={"manifest":{"id":"com.acme.crm",...},"status":"installed","enabled":true,...}   (bare row)
200  SCOPED   GET /api/v1/environments/env_alpha/packages            data={"packages":[...],"total":1}
404  SCOPED   DELETE /api/v1/environments/env_alpha/packages/no.such.package
       RESOURCE_NOT_FOUND :: Package 'no.such.package' not found
404  NEGATIVE CONTROL scoped   /environments/env_alpha/no-such-domain   ROUTE_NOT_FOUND
404  NEGATIVE CONTROL unscoped /no-such-domain                          ROUTE_NOT_FOUND
404  RETIRED /projects/ spelling                                        ROUTE_NOT_FOUND

⇒ A scoped /packages URL reaches the dispatcher's /packages domain through the catch-all (the dispatcher's spelling, single quotes, no trailing period). The fork the prior census reported has dissolved on today's tree; the three REST routes can be removed without any composition losing /packages.

Boundary — corrected by the at-tier review (fork report 5571983259), and the reason this PR was held in draft. The composition that loses scoped /packages under the deletion is reachable from the open-core CLI, not only from the cloud repo: serve.ts skips the standalone boot when shouldBootWithLibrary() is false (plugin-detection.ts — any host config, or OS_MODE=off) and then forwards api.enableProjectScoping verbatim to the REST plugin and the dispatcher plugin on HonoServerPlugin. With plugin-hono-server but without createHonoApp, the dispatcher's explicit mounts register /packages* at the unscoped prefix only and the transport's sole route into the dispatcher (setFallbackHandler) serves declarative apis: endpoints, so on that composition the REST mirror was the only door for scoped package reads and deletes — and after this PR there is none. My earlier boundary statement attributed that region to "a repo not in this session"; that attribution was wrong and is withdrawn here. Every consumer population reachable from this repo is zero for the scoped mount (measured with controls in the fork report); the residue is now stated in the changeset. The step-2 fork is ruled, not open: the director seat recorded C′ on #14503 (comment 5579647072, decision batch #84, 2026-09-08) — land this PR now with the residue disclosed in the changeset, and carry the repair on a follow-up card rather than inventing a scoped dispatcher mount in flight. On a plugin-hono-server composition with enableProjectScoping and no createHonoApp, the scoped /api/v1/environments/:id/packages[/:id] routes have no door until #16781 lands (ruled C′ on #14503). #16781 carries the scoped mountPackagesRoute variant — the automation / actions / ai pattern at dispatcher-plugin.ts:1653-1667, the same handler, no second implementation — together with the F2 response-shape reconciliation and a pin on that composition. ⛔ No scoped mountPackagesRoute variant is added here.

Step 3 — the deletion, and what pins the survivor

  • packages/rest/src/package-routes.ts: the three service-gated routes, their mount gate, the protocol option (only they read it), the read cohort of the gate, the registry projection helper and the compile-time pins for the protocol seam are removed; the docblocks say what happened and why. direct-mount-composition.ts and rest-api-plugin.ts stop threading protocol.
  • packages/rest/src/rest-route-ledger.ts: the three direct-mount rows are gone; the runtime route ledger carries the family.
  • packages/rest/src/rest-server.ts: /discovery's routes.packages is now derived from the publish mount (POST {base}/packages/publish{base}/packages) instead of from the registrar's own GET {base}/packages copy. Measured consequence worth knowing: on a stock objectstack serve boot that copy was never mounted (the package service registers after the REST plugin starts), so REST's discovery omitted routes.packages while the dispatcher served the family; it is advertised on every boot now.
  • packages/runtime/src/domains/packages-single-door.test.ts (new) pins the surviving door — 404 wording Package 'ID' not found with RESOURCE_NOT_FOUND, the retired spelling asserted absent, the bare-row envelope { data, meta, success } with no package wrapper and no source, the list rows without source — unscoped and environment-scoped, driven through dispatch() with the catch-all-derived subpath (the fix(runtime): the dispatcher's scope strip matches /environments/, the prefix its own hint parser reads #15859 pattern; the hono package's suite aliases runtime to a mock, so the real-adapter drive above is the measurement and this file is the durable pin).
  • packages/runtime/src/domains/packages.ts: the stale "which shadows it only when a package service is registered" sentence and the two "sibling REST door" references are corrected; behaviour untouched (main's fix(runtime): key package lifecycle state by project, not by environment alone #16572 merged cleanly beside it).
  • packages/rest/src/query-multiplicity.ts header: the "dormant" claim replaced with the measured fact (plugin-hono-server's readQuery keeps arrays since 两个 IHttpServer 适配器对「重复的查询参数」给出不同形状:Hono 折叠成第一个值,node:http 给数组 #6878 / PR test(http-conformance): 「重复查询参数」的跨适配器分歧落锁为已知事实 (#6878 路线 1) #6941, pinned by the http-conformance suite).
  • 8 REST pin files whose only subject was the removed routes are deleted; 12 are re-pointed at the one route left (the resolver-instrument suites now drive POST /packages/publish — same resolver, same gate, same three wire answers).
  • scripts/check-undeclared-dep-imports.mjs: the type-only ledger row for package-routes.ts@objectstack/metadata-protocol is deleted (the import went with the delete route; the gate reds on a stale row).
  • content/docs/permissions/system-context.mdx and scripts/doc-authoring-prose-id.baseline.json: line-rot re-anchoring and the shrink-only baseline regeneration, both by the gates' own --fix / --census-ledger remedies.

Changeset

@objectstack/rest minor (.changeset/rest-package-routes-single-implementation.md), per the ruling. It states FROM → TO for GET /packages/:id (data.packagedata), records the source stamp and the ?version= read + multiplicity refusal as deliberately removed, the 404 wording change, the DELETE semantics, and the discovery change.

Round 5 adds the contract review's two required rows and the ruling's residue sentence: F1 — capability refusals on the three routes answer 403 PERMISSION_DENIED where the removed REST copies answered 403 FORBIDDEN (deps.error(message, 403) with no code, error-envelope.ts deriving it via standardErrorCodeForHttpStatus), same status and same message text; F2 — the spec-conformance claim is scoped to GET /packages/:id alone, with the list's missing hasMore and the uninstall's missing packageId named as pre-existing drift that this release does not introduce and does not change, carded on #16781; and the C′ residue stated verbatim.

Verification

Every build/test ran through scripts/pm/os-verify-lock.sh; VERDICT lines read, never a bare $?. Heads are named per reading because the branch grew across the run (final head 60f7d46b49; the two commits after 7647624a00 touch only a docs page and a baseline JSON).

  • Closure build (pnpm --workspace-concurrency=2 --filter '@objectstack/hono^...' build, tree ce8caba914): build-exit=0, 97 "Build success" lines.
  • @objectstack/rest, full suite at 7647624a00 (after merging origin/main f2f6684cd5): Test Files 179 passed (179) · Tests 3008 passed (3008), exit 0.
  • @objectstack/runtime, full suite at 7647624a00: Test Files 239 passed (239) · Tests 3330 passed (3330), exit 0.
  • Touched pins, targeted at 74b480158b (18 rest files + the new runtime pin): 18 passed / 360 tests and 1 passed / 10 tests.
  • @objectstack/client ledger consumers at 60f7d46b49, against the rebuilt rest dist (rest-route-ledger-coverage, client-url-conformance, route-ledger-response-schema, packages-write-envelope, return-type-precision): 5 passed / 14 tests.
  • Typecheck (pnpm --filter @objectstack/rest typecheck, pnpm --filter @objectstack/runtime typecheck = tsc + check:test-typecheck) at 60f7d46b49: both exit 0. (A first attempt was NOT MEASURED — TS7016 against a packages/spec/dist that had lost its declarations mid-run; rebuilt with pnpm --filter @objectstack/spec build, check-dts-emitted: 34/34, then measured.)
  • Spec after the merge: pnpm --filter @objectstack/spec build && check:generated → "All 15 generated artifacts are up to date".
  • Ablation at 60f7d46b49 (committed state; trap-restored; both blobs hash-equal to HEAD afterwards, git diff HEAD empty): mutating the dispatcher's 404 spelling at its 6 sites turned the new runtime pin RED (4 failed / 6 passed — exactly the four wording cases, scoped and unscoped); resurrecting a GET /packages route in the registrar turned the REST absence pins RED (12 failed / 18 passed across package-publish-mount, direct-mount-introspection, rest-route-ledger.conformance). Both modules resolve from src through relative imports, so no dist sits on the path and no build leg was owed.
  • Gates: roster derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths); 79 families at f904e2d62e, 106 at 60f7d46b49 once the docs page joined the diff. Reconciliation --ran: 106 derived, 106 run, 0 UNRUN. Green, except four NOT MEASURED (reported as such, not as passes): check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET — needs a full pnpm build), check:type-check-debt (exit 3, PREREQUISITE NOT MET — needs the full built closure), check:pm-dispatch-gates (timed out at the 170 s and 280 s per-command budgets), check:skill-examples (exit 1 — refuses without @objectstack/client-react built; unrelated to this diff). Three gates were red on the raw diff and are green after their own remedies, committed here: check:system-context-census (line rot, --fix), check:doc-authoring (prose-id baseline over-pinned the deleted ledger row, --census-ledger shrink), check:undeclared-dep-imports (stale type-only row deleted with the import; its --self-test and real run both pass). The dispatch-named check:nul-bytes: green.
  • Lint, narrowed and proven: pnpm exec eslint --no-inline-config --format json over every .ts/.mjs this diff adds or modifies — ① population: the repo's eslint . --no-inline-config with ignores: [...NEVER_LINTED] (eslint.config.mjs); ② --format json count: 44 files (the changed set after the merge, a superset of this branch's own 27), 0 errors, 0 warnings, exit 0, at 60f7d46b49; ③ invariance: the config enables no parserOptions.project and no typed @typescript-eslint rules (eslint.config.mjs:328), so nothing in this diff can move an untouched file's verdict.
  • Declared narrowing, for CI: turbo ls --affected lists 19 packages; only rest, runtime and the client ledger consumers were run here. The others (cli, client-react, cloud-connection, dogfood, downstream-contract, four examples, hono, http-conformance, organizations, plugin-auth, plugin-dev, service-sms, verify) consume rest/runtime and are left to CI — the behavioural change is confined to the REST registrar and its discovery projection, and the hono adapter's own suite aliases runtime to a mock.
  • CI: in_progress at the time of this body — head ba6b8311ed (round 4: the census note's tracker id moved from the runtime string to an adjacent comment; pnpm check:doc-authoring exit 0 measured on this exact tree before the push) started 31 check runs (11 success, 18 in progress, 2 skipped, read 80 s after the push). On c1344677dc everything else was green — Dogfood Regression Gate passed (the authz-conformance discover() reaches 1 of 17 route registrars in rest-server.ts — the ratchet's route-completeness guarantee is false for 66 of 85 mounted routes #13260 census fix confirmed by CI) — and Lint & Repo Gates was red only on check:doc-authoring for that id-in-string. ⚠️ My round-3 report quoted check:doc-authoring 0; that reading was taken on the docs tree (edcb5808e7) before the census note existed and was carried forward unchecked — an ordering mistake on my side, not a CI/local divergence. Earlier history: 40c7679eec red on Dogfood 3/3 (the census); edcb5808e7 cancelled by the next push; the initial draft head 60f7d46b49 had 0 check runs.

Round 2 — contract-review follow-ups (all owed under any ruling outcome)

  1. Merge conflictorigin/main (8341ed21ce) merged; the one conflict, content/docs/permissions/system-context.mdx, resolved by taking main's side (symbolic file#function anchors) and re-running pnpm gen:system-context-census on the merged tree (no further re-anchoring needed; gate green). rest-server.ts auto-merged — main's change there is a comment hunk near the /meta read door, far from the discovery hunks. ⛔ No hand-edited anchors, no rebase, no force-push.
  2. Changeset accuracy — three additions, each spelled as deliberately removed / narrowed, never silently dropped: the catch-all sentence now carries its residue (the plugin-hono-server-without-createHonoApp composition above); the uninstall's tenancy width narrows from the REST copy's deletePackage({ packageId, allTenants: true }) (package-wide, protocol.deletePackage finds zero sys_metadata rows the data plane finds 3 of — uninstall leaves orphaned rows (persistence half of #7557) #7705 case 4) to the dispatcher's { packageId, organizationId?, keepData? } resolved for the caller's organization; GET /packages (not only /:id) no longer merges durable sys_packages rows, so a published-but-not-installed artifact is neither listed nor gettable on the surviving door. Level unchanged: @objectstack/rest minor.
  3. Census row 54 — the REST package gate is described as the write-only POST /packages/publish gate (manage_metadata); the read cohort is cited to the dispatcher domain's own gate by symbol (the census gate holds numeric row references to declared keys, so no row number is used).
  4. Clause-② pair check — re-run after the push; exit code in the report.

Round 3 — docs-drift (bot comment on this PR, 34 hand-written pages + 4 release-owned)

Re-derived on this tree with node scripts/docs-audit/affected-docs.mjs --json 8341ed21ce (38 rows, anchors read per row). Fixed only what this diff falsified:

  • content/docs/kernel/contracts/metadata-service.mdx — the REST endpoints table said the REST layer "mounts package routes under /api/v1/packages" and listed GET /api/v1/packages as "List all packages (registry + database)". The family is the dispatcher domain's now, the list is the installed set (published-but-not-installed artifacts are not merged in), the read answers the bare row with the dispatcher's 404 wording, the uninstall is organization-scoped. While editing that table, its POST /api/v1/packages "Publish" row was corrected to POST /api/v1/packages/publish — pre-existing (the bare POST is the install route), not something this diff falsified, fixed because the row is the one route this PR keeps.
  • content/docs/permissions/permission-sets.mdx — "the uninstall response reports the revocation under cleanups" described the removed REST envelope; on the surviving door it is data.persisted.cleanups.

Judged accurate and left alone (listed because they name a symbol or literal this diff touched, not because they went false): api/metadata-api.mdx (its GET /packages "List installed packages", GET /packages/:id, DELETE /packages/:id, PATCH …/enable|disable sections describe the surviving door; its POST /packages/publish response is shown flat — pre-existing, untouched); api/client-sdk.mdx (the client.packages.* snippet is the dispatcher door; its VALIDATION_ERROR row is the code's general meaning); api/environment-routing.mdx (env.packages.list() on the scoped client — the residue is a maintainer decision, stated in the changeset, not a docs edit); api/error-catalog.mdx, api/error-handling-client.mdx, api/error-handling-server.mdx, automation/webhooks.mdx, data-modeling/drivers.mdx, protocol/kernel/error-handling.mdx, protocol/objectql/types.mdx, ui/forms.mdx (anchored by the RESOURCE_NOT_FOUND / VALIDATION_ERROR literals — vocabulary pages); automation/flows.mdx, protocol/objectui/actions.mdx, ui/actions.mdx (anchored by errorMessage, a field name of theirs unrelated to the removed projection list); concepts/metadata-lifecycle.mdx, deployment/environment-variables.mdx, kernel/services-checklist.mdx (getMetaItems — the protocol verb is unchanged; the checklist's "packages is answered kernel-side by the /packages dispatcher domain" is now exactly true); automation/approvals.mdx, deployment/cli.mdx, permissions/capabilities.mdx, permissions/access-recipes.mdx, ui/audience-based-interfaces.mdx (manage_metadata — the write cohort is unchanged; none states the REST package read gate); permissions/authentication.mdx, plugins/packages.mdx (createRestApiPlugin — signature unchanged; the service-package blurb describes the service, not the route); data-modeling/formulas.mdx, deployment/publish-and-preview.mdx (/api/v1/cloud/packages is a different surface), deployment/self-hosting.mdx, getting-started/examples.mdx (GET /api/v1/packages lists the installed set — true), permissions/record-view-auditing.mdx, plugins/adding-a-metadata-type.mdx, protocol/kernel/http-protocol.mdx (the discovery example's "packages": "/api/v1/packages" is what is advertised), ui/apps.mdx (an endpoint: '/api/v1/packages' example — served); permissions/system-context.mdx (row 54 already corrected in round 2).

Release-owned, not touched (content/docs/releases/): implementation-status.mdx (anchored by registerDiscoveryEndpoints; its discovery sentence is generic and still true), v15.mdx (PATCH /packages/:id history), v16.mdx (manage_metadata), v17.mdx (the #7780 allTenants note is about the protocol verb, still true). None reads false on this diff; nothing for a docs-only PR.

The two declared blind spots, swept by hand: no hand-written page documents the ?version= repeated-parameter refusal on the package routes (query-multiplicity.ts yielded no anchor; the only repeated query param mention is client-sdk.mdx's general VALIDATION_ERROR row, which the /meta and /data read points still honour); the package client methods appear only in client-sdk.mdx (unchanged behaviour), metadata-api.mdx (packages.install) and environment-routing.mdx (above).

Docs gates on the tree: check:doc-anchors, check:docs-single-h1, check:doc-authoring, check-doc-route-spelling --advisory, check:docs-transcript-drift, check:docs-redirects, check:system-context-census, check:nul-bytes — exits in the report.

Round 3b — the #13260 authz-probe blind-spot census (CI red on 40c7679eec, Dogfood Regression Gate 3/3)

packages/qa/dogfood/test/authz-probe-blind-spot.census.ts pins population / reachable / blindSpot per probe file; the three ledger rows this PR removes moved the packages/rest/src/rest-route-ledger.ts row. The census has no generator, so all three numbers were re-derived from the tree with the module's own deriveProbeFileCensus() (a tsx script over every row, diffed against the recorded table) — ⛔ not back-solved from the assertion: derived population 91, reachable 91, controls route: ' 91 / family: ' 91 / RestRouteLedgerEntry 2. reachable moved with population (94 → 91 on both), because each removed row carried family: 'packages' and was reached by that family's key; the blind spot stays 0, the 19 families are unchanged (the publish row keeps the family), and BLIND_SPOT_TOTAL_STATIC / _RUNTIME (75 / 80, summed over the route surfaces, not the ledger rows) are untouched. After the edit every one of the 13 rows derives same. The row's note and the header's "94 audited rows" record the re-measurement. The aggregator's second red (dogfood-3-of-3 MISSING) is this shard's missing attestation, not a second cause.

Acceptance notes

  • File surface, beyond the four files the claim names (each a consequence of the deletion, stated here rather than left implicit): direct-mount-composition.ts and rest-api-plugin.ts (the protocol option only the removed routes read); rest-route-ledger.ts (the three rows); rest-server.ts (the routes.packages projection was keyed on the removed list route); scripts/check-undeclared-dep-imports.mjs (stale row); content/docs/permissions/system-context.mdx and scripts/doc-authoring-prose-id.baseline.json (gate remedies); 8 REST pin files deleted, 13 re-pointed, 1 runtime pin added; the changeset.
  • noted, not filed — history prose that names the retired door as its instrument and is left as history: packages/rest/src/rest-server.ts (two measurement narratives on GET /api/v1/packages), packages/runtime/src/package-door-error-parity.test.ts (header says the registrar "mounts FIRST in the production stack").
  • noted, already carded (client.packages declares envelopes two mounted surfaces do not emit: get diverges between them, and install/enable/disable declare { package } where the only serving surface sends the bare row #12034, pm:awaiting-maintainer)packages/client/src/index.ts still says only the REST registrar serves the scoped mount and declares ScopedEnvironmentClient.packages.get as { package: InstalledPackage } with a version? argument; both describe the removed door. That card holds the maintainer's 2026-08-26 ruling to bind the bare row; this PR is the producer-side convergence it was waiting on.
  • noted, not filed — mid-run, packages/spec/dist was rewritten without declaration files (dist mtime inside the check:* batch window; not attributed to a specific gate), which reds any later typecheck in the same tree with TS7016 until the package is rebuilt. Recovered by rebuilding spec; recorded so the next tree does not read the TS7016 as its own.
  • #12034 remains open — this PR does not close it.

维护者速读(草稿)

改了什么:/api/v1/packages 的读与删三条路由只剩一套实现(runtime dispatcher 的 /packages domain);@objectstack/rest 只再挂 POST /packages/publish。REST 那一套自带的 source 戳与 ?version= 读取随之删除,并在 changeset 里记为有意去掉。REST /discovery 改由 publish 挂载点推导 routes.packages

为什么改:同一网址两套实现且行为已分歧(404 措辞、data.package 外壳、source 戳),注释宣称的「遮蔽」在标准启动上从未发生(服务注册晚于 REST 挂载判定,三条路由根本没挂)。2026-09-02 裁决 A:删一套,不修优先级。

风险与代价(含回滚):曾经真的走到 REST 那一套的部署(package 服务先于 REST 启动注册的宿主)会看到 GET /packages/:id 的响应从 data.package 变成 data、404 措辞变化、?version= 不再被读取;in-tree 的所有组合均不受影响(标准启动本来就是 dispatcher 在答)。SDK 的 scoped packages.get(id, version?) 声明的 { package } 类型随之失真,归 #12034 那张已裁未合的卡。回滚 = revert 本 PR(纯删除 + 注释,无数据迁移)。

席位意见:(留空)

你要做的:确认 changeset 的 minor 定级与「有意去掉」两项;决定 #12034 的 SDK 收口是否随本 PR 排队。


Generated by Claude Code

…ackageRoutes (#14503)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
…t; pins follow the one route

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
…lot-lookups pin follows

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
… baseline after the ledger row deletion

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
…ckage-routes-single-implementation

# Conflicts:
#	content/docs/permissions/system-context.mdx
… 54 names the write-only gate

- changeset: the scoped mount is served by the dispatcher only where the
  @objectstack/hono catch-all is mounted; the uninstall's tenancy width
  narrows from allTenants to the caller's organization; GET /packages no
  longer merges durable sys_packages rows — each stated as deliberately
  removed, never silently dropped
- content/docs/permissions/system-context.mdx row 54: the REST package gate
  guards POST /packages/publish (manage_metadata) only; the read cohort is
  row 55's

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
The REST registrar mounts POST /packages/publish only (#14503); the read
cohort is the dispatcher domain's to enforce and is named in plain words,
since the census gate reads a path#symbol in row prose as an anchor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Sep 7, 2026
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/rest, @objectstack/runtime, touching 46 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/rest/src/query-multiplicity.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

34 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json b38821d1ce220527e2a7f34e254a96c48e2a9ba3.

4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/rest/src/query-multiplicity.ts) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 29 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b38821d1ce220527e2a7f34e254a96c48e2a9ba3packageMentionDocs.

Which tree this was computed on

This run read content/docs from 57a8c6a9e7f51e73e55c3955d803541a4254e813 — the merge of head a9f826933de660fc7aa51551132e96ab9f186f6d into base b38821d1ce220527e2a7f34e254a96c48e2a9ba3, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 57a8c6a9e7f51e73e55c3955d803541a4254e813 && git checkout 57a8c6a9e7f51e73e55c3955d803541a4254e813
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b38821d1ce220527e2a7f34e254a96c48e2a9ba3 a9f826933de660fc7aa51551132e96ab9f186f6d && git checkout -B drift-repro b38821d1ce220527e2a7f34e254a96c48e2a9ba3 && git merge --no-ff a9f826933de660fc7aa51551132e96ab9f186f6d

node scripts/docs-audit/affected-docs.mjs --json b38821d1ce220527e2a7f34e254a96c48e2a9ba3

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs b38821d1ce220527e2a7f34e254a96c48e2a9ba3 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

- kernel/contracts/metadata-service.mdx: the /api/v1/packages family is the
  dispatcher domain's; the REST registrar contributes POST /packages/publish
  (the table had it at the bare POST /packages, which is the install route);
  the list is the installed set, not registry + database; the read answers
  the bare row and the dispatcher's 404 wording; the uninstall is
  organization-scoped
- permissions/permission-sets.mdx: the uninstall's revocation report sits
  under data.persisted.cleanups on the surviving envelope

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
… 91/91 after the three package rows left (#14503)

population 94 -> 91 and reachable 94 -> 91, derived from the tree with
deriveProbeFileCensus(), not back-solved: each removed row carried
family: 'packages', so the family key reached it; the blind spot stays 0
and the 19 families are unchanged (the publish row keeps the family).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
…es the tracker id to a comment

check:doc-authoring forbids an issue id inside a runtime string literal; the
adjacent comment carries it for the reader who can resolve it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ

Copy link
Copy Markdown
Contributor

Contract review (CONTRACT_REVIEW_TIER, isolated seat) — verdict: PASS WITH FINDINGS

Ruling implemented: yes — option A of #14503, in the ruled order (measure → verify compositions → delete with the survivor pinned). Landing stays gated on the maintainer's answer to the step-2 fork report (5571983259, options A′/B′/C′), which this PR correctly escalated instead of resolving; the draft state is right until that is ruled. Nothing wider than A was taken (no scoped mountPackagesRoute variant, no dispatcher behaviour change); nothing narrower (all three routes are gone, the SHADOWS sentence with them, the survivor pinned on both mounts).

Read: card #14503 and its ruling, the PR head ba6b8311ed fetched into a throwaway ref and read via git show, current origin/main 8ccf7a1df. The PR body was treated as a claim; every line below was measured on the tree.

1. The ruling, verbatim, and what the PR does with it

Ruled: A. The three service-gated routes in packages/rest/src/package-routes.ts that duplicate the dispatcher's /packages and /packages/:id twins are removed, together with the SHADOWS comment; packages/runtime/src/domains/packages.ts is the single implementation. One URL, one body. […] 1. Measure first […] What has a consumer is ported into the dispatcher; what has none is recorded in the changeset as deliberately removed, not silently dropped. 2. Verify that every composition mounting the REST package routes also mounts the dispatcher packages domain; a composition that would lose /packages under the deletion is a fork report to the decision inbox, not a unilateral choice. 3. Then delete, with the 404 wording and the envelope shape of the surviving door pinned so "which door answered" stays observable.

  • Step 1: census re-taken on the branch base with controls; source (0 readers) and ?version= multiplicity refusal (0 senders of the repeat; 1 sender of the parameter, the scoped SDK, carded client.packages declares envelopes two mounted surfaces do not emit: get diverges between them, and install/enable/disable declare { package } where the only serving surface sends the bare row #12034) — both recorded as deliberately removed in the changeset, nothing ported. Consistent with the ruling.
  • Step 2: the fork exists (scoped mount on plugin-hono-server without createHonoApp) and was filed to the decision inbox, not decided here. Consistent with the ruling.
  • Step 3: packages/rest/src/package-routes.ts on main mounted GET {base}/packages, GET {base}/packages/:id, DELETE {base}/packages/:id behind resolvePackageService() (main :944-946); on the head the registrar builds routes = [publishRoute] and nothing else (head :407 is the only path: left). The read cohort, the protocol seam, the _Pin* compile-time types (never re-exported from packages/rest/src/index.ts, so not public API), the registry projection helper and the ?version= reads all went with it.

2. "One implementation" — route census on the head

route registrations on head where
GET /packages 1 packages/runtime/src/domains/packages.ts:573 via dispatcher-plugin.ts:1306 (mountPackagesRoute, unscoped) and the @objectstack/hono catch-all
GET /packages/:id 1 packages/runtime/src/domains/packages.ts:1198
DELETE /packages/:id 1 packages/runtime/src/domains/packages.ts:1255
POST /packages/publish 1 packages/rest/src/package-routes.ts:407 (unchanged)

packages/hono/src/*.ts and packages/plugins/plugin-hono-server/src/*.ts: zero /packages route registrations (grep, non-test). On main the REST registrar carried a second registration for each of the three (package-routes.ts:873, :967 and the list route). After the PR: exactly one each. Claim holds.

Anything silently gone? Unscoped: no — every in-tree composition that mounts the REST registrar also mounts the dispatcher (serve.ts, verify/harness.ts, plugin-dev), and on a stock boot the REST copies were never mounted (the gate ran before PackageServicePlugin registered), so the dispatcher was already the door. Scoped without createHonoApp: yes — those three URLs now fall to the transport's bare 404 with no refusal envelope. This is stated in the changeset and is the open fork; it is the maintainer's call, not a defect of this PR.

3. Wire contract on the surviving door

  • GET /packages/:id{ success, data: <InstalledPackage row>, meta }. Matches GetInstalledPackageResponseSchema (packages/spec/src/api/package-api.zod.ts:96-98, data: InstalledPackageSchema bare). FROM data.package + source TO data — stated in the changeset. ✅
  • 404 on both GET and DELETE → deps.error("Package '<id>' not found", 404)error.code = RESOURCE_NOT_FOUND via packages/runtime/src/error-envelope.ts:120 (standardErrorCodeForHttpStatus, errors.zod.ts:176). ADR-0112 envelope holds; wording change stated. ✅
  • Anonymous refusal: both doors used shouldDenyAnonymous (packages.ts:552; removed REST refusePackageRequest). Same status/code. ✅
  • Capability refusal code changed and is not in the changeset — see F1.
  • GET /packages and DELETE /packages/:id do not match their packages/spec declarations — see F2 (pre-existing on the dispatcher, but the survivor is now the only implementation, so it is the contract).

Findings

F1 — 403 refusal code changes FORBIDDENPERMISSION_DENIED on the three routes; undisclosed.
packages/runtime/src/domains/packages.ts:249-256 (requireManageMetadata) and :281-290 (requireReadCapability) call deps.error(message, 403) with no explicit code; packages/runtime/src/error-envelope.ts:120 then derives standardErrorCodeForHttpStatus(403) = PERMISSION_DENIED (packages/spec/src/api/errors.zod.ts:175). The removed REST copies emitted sendError(res, 403, 'FORBIDDEN', …) (main package-routes.ts, the refusePackageRequest write/read branches). Same status, same message text, different error.code — a client branching on err.code === 'FORBIDDEN' for a package read/delete refusal stops matching on any composition that reached the REST copies. Both codes are ADR-0112 standard members, so the envelope holds. Expectation: one row in .changeset/rest-package-routes-single-implementation.md: capability refusals on GET /packages, GET /packages/:id, DELETE /packages/:id answer 403 PERMISSION_DENIED (FROM 403 FORBIDDEN), messages unchanged. Level stays minor.

F2 — the "surviving shape is the spec's own" claim is true only for GET /:id; the list and uninstall shapes do not match spec, and nothing pins them.

  • GET /packages answers { packages, total } (packages.ts:585); ListInstalledPackagesResponseSchema (package-api.zod.ts:71-77) requires hasMore: z.boolean(), and declares inputs enabled/limit/cursor the domain does not read (it reads status and an undeclared type, :576-581).
  • DELETE /packages/:id answers { success, registryRemoved, persisted } (packages.ts:1296); UninstallPackageApiResponseSchema (package-api.zod.ts:358-364) requires packageId.
  • The runtime ledger rows for packages.list / packages.uninstall (packages/runtime/src/route-ledger.ts:336, :339) carry no responseSchema; grep finds no test outside packages/spec referencing either schema.
    This drift predates the PR (the REST copies did not match either), so it is not introduced here — but with one implementation there is now exactly one thing to reconcile. Expectation: (a) the changeset/PR body scope the spec-conformance sentence to GET /packages/:id; (b) a card, not a rider, for reconciling the list and uninstall doors with PackageApiContracts.listPackages / .uninstallPackage (or retiring those declarations under ADR-0049 if the dispatcher's shape is the ruled one).

F3 — scoped SDK binding is stale the moment this merges (disclosed, carded).
packages/client/src/index.ts:366-377: ScopedEnvironmentClient.packages.get(id, version?) returns { package: InstalledPackage } and its docblock says only the REST registrar serves the scoped mount. On this head the scoped door (through the catch-all) answers the bare row and ignores version. Held on #12034 (open, pm:awaiting-maintainer), acknowledged in the PR body. Expectation: #12034 queues in the same train, or its remaining half is named in the changeset as known-stale; either is acceptable. Not blocking.

4. Tests (read, not trusted)

  • Reddens if a second REST implementation reappears: packages/rest/src/package-publish-mount.test.ts:129-149 (registrar returns exactly ['POST /api/v1/packages/publish'] with and without a package service, and asserts the three former twins absent); packages/rest/src/direct-mount-introspection.test.ts (ledger has only the publish row; the twins are neither mounted nor in OpenAPI on either boot, scoped base included); packages/rest/src/rest-route-ledger.conformance.test.ts:153-181 (mounted direct-mount set ≡ ledger set in both directions, so a resurrected route without a row reds, and a stale row without a mount reds). ✅
  • Survivor pinned: packages/runtime/src/domains/packages-single-door.test.ts — 404 wording verbatim with RESOURCE_NOT_FOUND, retired spelling asserted absent, envelope key set ['data','meta','success'], no package wrapper, no source on row or list rows, unscoped and /environments/:id/… through dispatch() with the catch-all-derived subpath, two negative controls (ROUTE_NOT_FOUND). Identity goes through the real resolver, not a bypass. ✅
  • Per removed route "gone or refused": the REST absence pins above cover "gone" at the registrar and ledger. There is no pin that the scoped URL on the plugin-hono-server-only composition answers anything in particular — correct, because what it should answer is the open fork.
  • The 8 deleted REST pin files (package-delete-status-classification, package-door-producer-key-carry, package-id-registry-read-refusal, package-list-durable-read-refusal, package-list-registry-read-refusal, package-list-writable-carry, package-registry-item-projection, package-routes-query-multiplicity) pinned REST-copy-only behaviour (registry+durable merge, source, ?version=, REST's projection). Dispatcher-side equivalents exist for what survives: packages-writable-verdict.test.ts, packages-readonly-gate.test.ts, packages-serializable-response.test.ts, packages-uninstall-envelope.test.ts (PACKAGE_DELETE_PARTIAL). ✅
  • packages/runtime/src/domains/packages.ts diff is comment-only (verified line by line); "behaviour untouched" holds.

5. Changeset and governed paths

  • .changeset/rest-package-routes-single-implementation.md: @objectstack/rest minor — matches the ruling. FROM/TO stated for the /:id envelope, 404 wording, source, ?version= + refusal, durable-row merge, DELETE tenancy width (allTenants: true → caller's organization), discovery, and the scoped-mount residue. Missing: F1. No @objectstack/runtime changeset — correct (comments + a test).
  • Governed paths (docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md): none touched. content/docs/releases/: untouched. The three content/docs edits and packages/qa/dogfood/test/authz-probe-blind-spot.census.ts (94→91, reachable moved with population, blind spot 0) read accurate against the code.

6. CI and mergeability

  • Head ba6b8311ed: 37 check runs, all success except two skipped (Console Pin Gate, packed-tarball opt-in); Vercel status success. Lint & Repo Gates green on this head (the check:doc-authoring red was on c1344677dc, fixed).
  • GitHub reports mergeable_state: unknown. Measured: git merge-tree --write-tree origin/main(8ccf7a1df) <head> exits 0, no conflicts. The branch is 51 commits behind main (recorded base 8341ed21c); a merge from main before ready-for-review is hygiene, not a blocker.

Summary for the PM

  • Ruling A executed as ruled; step-2 fork correctly escalated (5571983259) — that ruling is the landing gate, not this review.
  • Required before ready: F1 (one changeset row). Recommended: F2(a) one sentence; F2(b) a card.
  • Not approving, not marking ready, no labels changed, no files edited.

Generated by Claude Code

…the C′ residue (#14503)

Contract review 5578894182 F1/F2 and the director seat's step-2 ruling C′
(#14503 comment 5579647072):

- F1: one row for the capability refusal `error.code` moving from the removed
  REST copies' explicit `403 FORBIDDEN` to the dispatcher's status-derived
  `403 PERMISSION_DENIED` (`standardErrorCodeForHttpStatus`), same status,
  same message text.
- F2: the spec-conformance claim is scoped to `GET /packages/:id`; the list's
  missing `hasMore` and the uninstall's missing `packageId` are named as
  pre-existing drift, carded on #16781.
- C′: the residue sentence — a `plugin-hono-server` composition with
  `enableProjectScoping` and no `createHonoApp` has no scoped `/packages` door
  until #16781 lands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TezFG8ZMrNH6n5VTNpPpdH
…blish door (#14503)

`origin/main` landed `package-door-16019-raw-statement-fault-code.test.ts`
while this branch was open. It pins the raw-statement declared-fault `code` at
BOTH REST package doors, and its two `DELETE /api/v1/packages/:id` cases drive
a route this branch removes -- the merge is clean textually and red
semantically (`Error: no handler for DELETE /api/v1/packages/:id`).

Resolved by keeping this PR's deletion: the two DELETE cases and the
`deleteWith` helper are removed, the docblock records why. They are not
re-pointed at the dispatcher -- the surviving door uninstalls through
`protocol.deletePackage` and the registry, never through
`PackageService.delete`, so the producer this file doubles has no delete door
left in `@objectstack/rest`. The producer-side half is untouched and still
pinned in `service-package`'s `delete-driver-fault.test.ts`.

`@objectstack/rest`: 184 files / 3057 tests pass (was 1 file / 2 tests red).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TezFG8ZMrNH6n5VTNpPpdH
@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Patch round — F1, F2 and the C′ residue (director seat ruling 5579647072)

Head a9f826933 (was ba6b8311ed). Dispatched by the director seat against the recorded step-2 ruling C′ (#14503, comment 5579647072): land now with the residue disclosed, #16781 carries the scoped mountPackagesRoute variant. ⛔ No scoped dispatcher mount is added here; nothing widened.

What changed

1. origin/main merged (3d35f8fcd, plain merge, base ed7243d52, 61 commits behind → 0). ⛔ No rebase, no amend, no force-push. No textual conflictgit merge exited 0 with nothing to resolve; no generator or --fix re-run was owed and no anchor was hand-edited.

2. One semantic conflict, resolved by keeping this PR's deletion (a9f826933). origin/main landed packages/rest/src/package-door-16019-raw-statement-fault-code.test.ts while this branch was open. It pins the #16019 raw-statement declared-fault code at both REST package doors, and its two DELETE /api/v1/packages/:id cases drive a route this branch removes: the merge is clean textually and red semantically — Error: no handler for DELETE /api/v1/packages/:id, 1 file / 2 tests. Resolution: the two DELETE cases and the deleteWith helper are removed and the docblock records why. They are not re-pointed at the dispatcher — the surviving door uninstalls through protocol.deletePackage and the registry, never through PackageService.delete, so the producer this file doubles has no delete door left in @objectstack/rest. The producer-side half is untouched and still pinned in service-package's delete-driver-fault.test.ts ([#16019] block). The publish half of the file is unchanged and still green.

3. F1 — capability-refusal error.code, one changeset row (d417f9464). Capability refusals on GET /packages, GET /packages/:id and DELETE /packages/:id answer 403 PERMISSION_DENIED; the removed REST copies emitted sendError(res, 403, 'FORBIDDEN', …). Measured on the merged tree: requireManageMetadata (packages/runtime/src/domains/packages.ts:254) and requireReadCapability (:287) call deps.error(message, 403) with no code, and packages/runtime/src/error-envelope.ts:120 derives one from the status via standardErrorCodeForHttpStatus, which maps 403 to PERMISSION_DENIED (packages/spec/src/api/errors.zod.ts:175). Same status, same message — both cohort strings ("Managing packages requires the manage_metadata capability." and "Reading packages requires the studio.access or setup.access capability.") are byte-identical on the two doors. Both codes are ADR-0112 standard members, so the envelope holds; what moves is that a client branching on err.code === 'FORBIDDEN' for a package read or delete refusal stops matching on any composition that really did reach the REST copies. Level unchanged: @objectstack/rest minor.

4. F2 — the spec-conformance claim scoped, the drift named as pre-existing (d417f9464). The changeset now claims spec conformance for GET /packages/:id only (its { success, data, meta } is GetInstalledPackageResponseSchema, data: InstalledPackageSchema bare). The other two are named as pre-existing drift that this release neither introduces nor changes, carded on #16781: GET /packages answers { packages, total } (packages.ts:599) while ListInstalledPackagesResponseSchema requires hasMore (package-api.zod.ts:76); DELETE /packages/:id answers { success, registryRemoved, persisted } (packages.ts:1354) while UninstallPackageApiResponseSchema requires packageId (package-api.zod.ts:360). The missing responseSchema on the runtime ledger rows for packages.list / packages.uninstall is named there too.

5. C′ residue, verbatim in the changeset (d417f9464): "On a plugin-hono-server composition with enableProjectScoping and no createHonoApp, the scoped /api/v1/environments/:id/packages[/:id] routes have no door until #16781 lands (ruled C′ on #14503)." Mirrored in the PR body's Boundary paragraph, where "the ruling's step 2 makes this the maintainer's call" is replaced by the recorded ruling (5579647072, C′) and #16781.

Tests — counts on a9f826933, all through scripts/pm/os-verify-lock.sh, VERDICT lines read

run verdict result
closure build, --filter '@objectstack/rest^...' --filter '@objectstack/runtime^...' command-exit 0 94 Build success lines
pnpm --filter @objectstack/rest test (before the fix) command-exit 1 Test Files 1 failed | 183 passed (184) · Tests 2 failed | 3057 passed (3059)
pnpm --filter @objectstack/rest test (after) command-exit 0 Test Files 184 passed (184) · Tests 3057 passed (3057)
pnpm --filter @objectstack/runtime test command-exit 0 Test Files 241 passed (241) · Tests 3350 passed (3350)
pnpm --filter @objectstack/rest typecheck command-exit 0 tsc + check:test-typecheck — 0 files / 0 errors in debt
pnpm --filter @objectstack/runtime typecheck command-exit 0 tsc + check:test-typecheck — 27 files / 191 errors held, unchanged

Gate exit codes — verbatim, redirected before capture, never a bare $?

gate exit
node scripts/check-changeset-no-major.mjs --base origin/main 0 (✓ This diff introduces no major bump; LEVEL AXIS: NOT MEASURED — no pull_request payload locally, #4690)
node scripts/check-changeset-no-major.mjs --self-test 0
node scripts/check-adr-0087-registration.mjs --base origin/main 0
node scripts/check-adr-0087-registration.mjs --self-test 0
node scripts/check-empty-changeset.mjs --base origin/main 0 (1 declaring changeset added)
node scripts/check-empty-changeset.mjs --self-test 0
node scripts/check-changeset-fixed.mjs 0
pnpm check:changeset-gate-self-tests 0
pnpm check:system-context-census 0 (106 read sites, 103 of 103 required symbols cited)
pnpm check:doc-authoring 0 (398 docs, 46 skill files, 15124 spec strings, baseline held)
pnpm check:nul-bytes 0 (8276 files, no raw control bytes)
pnpm check:test-source-alias 0
pnpm check:cross-package-test-inputs 0
pnpm check:objectui-changeset 0

The two that were red on the raw diff — check:system-context-census and check:doc-authoring — are 0 on this head. No exit 3 (NOT MEASURED) in this round; no gate is reported as green that was not measured.

Declared narrowing. This round changed two files (the changeset and one @objectstack/rest test). The roster node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derives for the full PR diff at this head is 145 commands; the families that read this round's two paths are the changeset family, nul-bytes, test-source-alias and cross-package-test-inputs, all run above, plus the two docs gates the dispatch named. The rest of the farm is left to CI, which is the run that owns it. origin/main advanced to b38821d1c during this round (2 commits) — the merge above is against ed7243d52.

Not done, deliberately


Generated by Claude Code

Copy link
Copy Markdown
Contributor

Contract review (CONTRACT_REVIEW_TIER, isolated seat) — PR #16628 @ a9f8269 — re-review

Verdict: PASS — conditional only on the CI run for a9f826933 finishing green (state at posting time in §6). Nothing in the tree blocks landing; the three prior findings are discharged, the semantic test conflict was resolved as a legitimate narrowing (not a skipped test), and no governed path is touched.

Read: head a9f826933de660fc7aa51551132e96ab9f186f6d fetched into a throwaway ref and read via git show / git diff (never checked out); merge-base with origin/main is ed7243d52; origin/main at read time b38821d1c. The PR body, the patch-round comment (5580072783) and the changeset were treated as claims; every line below was measured on the tree.

The ruling this re-review holds the PR to

#14503, comment 5579647072 (director seat, decision batch #84, 2026-09-08):

Ruled: C′ — land PR #16628 now with the residue disclosed in the changeset; card B′ as a follow-up. […] Execution for #16628 before landing (contract review 5578894182): F1 — one changeset row for the capability-refusal error.code 403 FORBIDDEN → 403 PERMISSION_DENIED; F2 — scope the "surviving shape is the spec's own" claim to GET /packages/:id and name the hasMore / packageId drift as pre-existing; changeset states the C′ residue verbatim ("a scoped composition without the catch-all has no /packages door until <B′ card>"); merge origin/main. Then ready + auto-merge by the director seat (non-governed, @objectstack/rest minor).

The ruling's ledger line says objectstack#12708; the B′ card actually filed and referenced by the changeset and body is #16781. The patch round disclosed that discrepancy itself; the tree references #16781 consistently, which is the card that exists.

Prior findings (review 5578894182 @ ba6b831) — discharged?

# finding expectation on a9f8269 status
F1 capability refusals answer 403 PERMISSION_DENIED where the REST copies answered 403 FORBIDDEN; undisclosed one changeset row, FROM/TO, level stays minor Row present ("Capability refusals answer a different error.code. … FROM 403 FORBIDDEN TO 403 PERMISSION_DENIED"). Derivation re-verified: packages/runtime/src/domains/packages.ts:254 (requireManageMetadata) and :287 (requireReadCapability) call deps.error(message, 403) with no code; packages/runtime/src/error-envelope.ts buildApiErrorinput.code ?? promoted ?? standardErrorCodeForHttpStatus(httpStatus); packages/spec/src/api/errors.zod.ts HttpStatusErrorCodeMap[403] = 'PERMISSION_DENIED'. The two cohort messages quoted in the row are byte-identical to the source strings. Level minor unchanged. discharged
F2 "surviving shape is the spec's own" true only for GET /:id; list lacks hasMore, uninstall lacks packageId; nothing pins them (a) scope the sentence; (b) a card (a) Changeset: "Spec conformance on the surviving door is claimed for GET /packages/:id only", then both drift cases named with schema and field (ListInstalledPackagesResponseSchema requires hasMore; UninstallPackageApiResponseSchema requires packageId), stated as pre-existing, not introduced by this release, and the missing responseSchema on the runtime ledger rows packages.list / packages.uninstall (packages/runtime/src/route-ledger.ts:336, :339 — confirmed still without responseSchema) named. (b) Carded on #16781. discharged
F3 scoped SDK packages.get(id, version?) stale on merge; carded #12034 name it or queue it; not blocking Changeset's ?version= row names the SDK sender and #12034. Unchanged from the prior review; was not blocking. discharged (as disclosed)

Also owed by the ruling: merge origin/main — done (3d35f8fcd, plain merge, base ed7243d52, no textual conflict; git merge-tree --write-tree origin/main a9f826933 against today's b38821d1c exits 0 as well). C′ residue verbatim — present as the last sentence of the changeset: "On a plugin-hono-server composition with enableProjectScoping and no createHonoApp, the scoped /api/v1/environments/:id/packages[/:id] routes have no door until #16781 lands (ruled C′ on #14503)."

Numbered verification

1. Diff surface and governed paths. git diff ed7243d52..a9f826933 --name-status: 38 files, +945 / −4032. 1 changeset added; 3 content/docs pages (none under releases/); 1 dogfood census; 26 files under packages/rest/src (8 test files deleted, 1 source-level module package-routes.ts cut 800 lines, ledger/server/composition/plugin edits, 13 tests re-pointed incl. the #16019 file); packages/runtime/src/domains/packages.ts (comment-only — diff read line by line, no executable hunk) plus the new packages-single-door.test.ts; 2 scripts. Governed paths (docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md, content/docs/releases/): none — grep over the name list returns nothing. Governed Surface Queue Guard: success.

2. The semantic conflict — package-door-16019-raw-statement-fault-code.test.ts.

3. Changeset (.changeset/rest-package-routes-single-implementation.md): @objectstack/rest: minor — matches the ruling. F1 row: present and accurate (derivation in the table above). F2 scoping: present, names both drift cases and #16781. C′ residue: present, verbatim in the ruled form with #16781 as the <B′ card>. All earlier rows (FROM data.package TO data, source removed, durable-row merge removed, ?version= + refusal removed, 404 wording, DELETE tenancy width, discovery, ledger rows) unchanged from the prior review. No @objectstack/runtime changeset — still correct (comments + a test). Check Changeset: success.

4. PR body "Boundary" paragraph: cites "the director seat recorded C′ on #14503 (comment 5579647072, decision batch #84, 2026-09-08)" and "#16781 carries the scoped mountPackagesRoute variant … together with the F2 response-shape reconciliation"; the earlier "maintainer's call" phrasing is gone; the withdrawn "a repo not in this session" attribution stays withdrawn. ✅

5. Route census on the head — unchanged from ba6b831.

route registrations where
GET /packages 1 packages/runtime/src/domains/packages.ts via dispatcher-plugin.ts:1322 (mountPackagesRoute('get', '')) + the @objectstack/hono catch-all
GET /packages/:id 1 packages.ts:~1198 via dispatcher-plugin.ts:1325
DELETE /packages/:id 1 packages.ts:1255 via dispatcher-plugin.ts:1326
POST /packages/publish 1 packages/rest/src/package-routes.ts:406-407; ledger row rest-route-ledger.ts:453 is the only packages row left

packages/hono/src/*.ts and packages/plugins/plugin-hono-server/src/*.ts (non-test): zero /packages registrations. Every other /packages mention in packages/rest/src non-test files is docblock prose describing the removal.

6. CI on a9f826933 (read at posting time, 35 check runs): 24 success, 2 skipped (Console Pin Gate, Packed-tarball smoke opt-in), 9 in_progress, 0 failure. Still running at 06:06Z: Test Core 4/6 and 5/6, Dogfood Regression Gate 1/3–3/3, Temporal Conformance (live PG + MySQL), Build Core, Dogfood Verify CLI, Lint & Repo Gates. Already green: all four Type Check jobs, Test Core 1/2/3/6, Build Docs, Check Changeset, Governed Surface Queue Guard, the three claim/part-of guards, Spec property liveness, docs links. The verdict above is conditional on those nine completing green; the two that were red on earlier heads (Dogfood — the #13260 census; Lint & Repo Gates — check:doc-authoring) were green on ba6b831 and the round-5 diff touches neither input. mergeable_state reported blocked — that is the draft/required-review state, not a conflict; merge-tree against b38821d1c is clean.

Residual findings

R1 — informational, not blocking: the surviving DELETE /packages/:id cannot surface a sys_packages driver fault at all. Consequence of the reading in §2: on the one implementation, a durable-row delete that fails (no such table, FK restriction, lock timeout — the exact family delete-driver-fault.test.ts reproduces) is logged by protocol.deletePackage and the door still answers 200 { success: true, registryRemoved, persisted } — and per the #2532 comment at that site, the row left behind re-hydrates the package on the next restart. Pre-existing dispatcher behaviour, reachable on every stock boot before this PR, so not introduced here and not a defect of this PR; the REST copy that could answer 500 DATABASE_ERROR for it was never mounted on a stock boot. Expectation: none for landing. Recommended: one line on #16781 (which already owns the uninstall-shape reconciliation) or on #16019 noting that with the REST door gone, the declared delete fault has a producer pin but no wire-observable door, so the card's "two doors" narrative is now one door with a swallow.

R2 — housekeeping, not blocking: the ruling comment's ledger line reads objectstack#12708 while the filed card is #16781. Already disclosed by the patch round; the tree is consistent on #16781. Expectation: the director seat corrects the ledger line on #14503 if it is load-bearing anywhere; nothing owed in this PR.

Summary for the director seat


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 8, 2026 06:08
@os-zhuang
os-zhuang enabled auto-merge September 8, 2026 06:08
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit 7370989 Sep 8, 2026
42 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-14503-package-routes-single-implementation branch September 8, 2026 06:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants